# Copyright (c) 2018 Presto Labs Pte. Ltd.
# Author: xguo

import functools
import os
import requests
import logging

from coin.exchange.binance_futures.enum_types import (
    BinanceFuturesOrderSide,
    BinanceFuturesOrderType,
    BinanceFuturesTimeInForce,
)
from coin.exchange.binance.kr_rest.native_private_client import (
    BinanceSyncQueryer,
    BinanceAsyncQueryer,
    binance_req_timestamp,
)


class BinanceUrlCache(object):
  @property
  @functools.lru_cache()
  def BINANCE_SWAP_API_URL(self):
    def is_whitelisted():
      fapi2_url = 'https://fapi2.binance.com/fapi/v1/ticker/price'
      try:
        # Usually a timeout error if not whitelisted
        r = requests.get(fapi2_url, timeout=2)
        r.raise_for_status()  # Just in case HTTP status error
        return True
      except Exception:
        return False

    if is_whitelisted():
      return 'fapi2.binance.com'
    else:
      return 'fapi.binance.com'

    if os.environ.get('USE_BINANCE_TESTNET'):
      return 'testnet.binancefuture.com'


bns_url = BinanceUrlCache()


class BinanceSwapSyncQueryer(BinanceSyncQueryer):
  def __init__(self, key_file, *, api_host=None, timeout=10):
    super().__init__(key_file, api_host=api_host, timeout=timeout)
    self._api_host = "https://" + (api_host or bns_url.BINANCE_SWAP_API_URL)
    logging.info('[BinanceSwapSyncQueryer]: using api_host %s', self._api_host)


class BinanceSwapAsyncQueryer(BinanceAsyncQueryer):
  def __init__(self, key_file, *, api_host=None, timeout=10, max_connections=4):
    super().__init__(key_file, api_host=api_host, timeout=timeout, max_connections=max_connections)
    self._api_host = "https://" + (api_host or bns_url.BINANCE_SWAP_API_URL)
    logging.info('[BinanceSwapAsyncQueryer]: using api_host %s', self._api_host)


class BinanceSwapNativePrivateClient(object):
  def __init__(
      self,
      *,
      key_file,
      api_host=None,
      timeout=10,
      max_connections=8,
      recv_window=None,  # default to 5000
      use_async=False,
  ):
    if use_async:
      self._auth_queryer = BinanceSwapAsyncQueryer(key_file,
                                                      api_host=api_host,
                                                      timeout=timeout,
                                                      max_connections=max_connections)
    else:
      self._auth_queryer = BinanceSwapSyncQueryer(key_file, api_host=api_host, timeout=timeout)

    self._recv_window = recv_window

  def _query(self, method, path, params=None):
    return self._auth_queryer.query(method, path, params=params)

  def place_order(
      self,
      *,
      symbol,
      price,
      quantity,
      order_side: BinanceFuturesOrderSide,
      order_type: BinanceFuturesOrderType,
      time_in_force=None,
      new_client_order_id=None,
      stop_price=None,
  ):
    method = 'POST'
    path = '/fapi/v1/order'

    assert isinstance(order_side, BinanceFuturesOrderSide), type(order_side)
    assert isinstance(order_type, BinanceFuturesOrderType), type(order_type)

    if time_in_force is not None:
      assert isinstance(time_in_force, BinanceFuturesTimeInForce), type(time_in_force)
      time_in_force = time_in_force.name
    else:
      if order_type == BinanceFuturesOrderType.LIMIT:
        time_in_force = 'GTC'
      elif order_type == BinanceFuturesOrderType.LIMIT_MAKER:
        pass
      else:
        raise ValueError("not supported order_type: %s" % order_type)

    params = {
        'symbol': symbol,
        'side': order_side.name,
        'type': order_type.name,
        'timeInForce': time_in_force,
        'quantity': quantity,
        'price': price,
        'newClientOrderId': new_client_order_id,
        'stopPrice': stop_price,
        'recvWindow': self._recv_window,
        'timestamp': binance_req_timestamp(),
    }
    update = self._query(method, path, params)
    return update

  def place_test_order(
      self,
      *,
      symbol,
      price,
      quantity,
      order_side: BinanceFuturesOrderSide,
      order_type: BinanceFuturesOrderType,
      time_in_force=None,
      new_client_order_id=None,
      stop_price=None,
  ):
    method = 'POST'
    path = '/fapi/v1/order/test'

    assert isinstance(order_side, BinanceFuturesOrderSide), type(order_side)
    assert isinstance(order_type, BinanceFuturesOrderType), type(order_type)

    if time_in_force is not None:
      assert isinstance(time_in_force, BinanceFuturesTimeInForce), type(time_in_force)
      time_in_force = time_in_force.name
    else:
      if order_type == BinanceFuturesOrderType.LIMIT:
        time_in_force = 'GTC'
      elif order_type == BinanceFuturesOrderType.LIMIT_MAKER:
        pass
      else:
        raise ValueError("not supported order_type: %s" % order_type)

    params = {
        'symbol': symbol,
        'side': order_side.name,
        'type': order_type.name,
        'timeInForce': time_in_force,
        'quantity': quantity,
        'price': price,
        'newClientOrderId': new_client_order_id,
        'stopPrice': stop_price,
        'recvWindow': self._recv_window,
        'timestamp': binance_req_timestamp(),
    }
    update = self._query(method, path, params)
    return update

  def query_order(
      self,
      symbol,
      order_id=None,
      orig_client_order_id=None,
  ):
    method = 'GET'
    path = '/fapi/v1/order'
    params = {
        'symbol': symbol,
        'orderId': order_id,
        'origClientOrderId': orig_client_order_id,
        'recvWindow': self._recv_window,
        'timestamp': binance_req_timestamp(),
    }
    update = self._query(method, path, params)
    return update

  def cancel_order(
      self,
      symbol,
      order_id=None,
      orig_client_order_id=None,
      new_client_order_id=None,
  ):
    method = 'DELETE'
    path = '/fapi/v1/order'

    # Either orderId or origClientOrderId must be sent.
    assert order_id or orig_client_order_id, (order_id, orig_client_order_id)

    params = {
        'symbol': symbol,
        'orderId': order_id,
        'origClientOrderId': orig_client_order_id,
        'newClientOrderId': new_client_order_id,
        'recvWindow': self._recv_window,
        'timestamp': binance_req_timestamp(),
    }
    update = self._query(method, path, params)
    return update

  def query_open_orders(self, symbol):
    method = 'GET'
    path = '/fapi/v1/openOrders'
    params = {
        'symbol': symbol,
        'recvWindow': self._recv_window,
        'timestamp': binance_req_timestamp(),
    }
    update = self._query(method, path, params)
    return update

  def query_all_orders(
      self,
      symbol,
      order_id=None,
      start_time=None,
      end_time=None,
      limit=None,
  ):
    method = 'GET'
    path = '/fapi/v1/allOrders'

    if limit is not None:
      assert 0 < limit <= 1000, limit

    params = {
        'symbol': symbol,
        'order_id': order_id,
        'start_time': start_time,
        'end_time': end_time,
        'limit': limit,
        'recvWindow': self._recv_window,
        'timestamp': binance_req_timestamp(),
    }
    update = self._query(method, path, params)
    return update

  def query_account(self):
    method = 'GET'
    path = '/fapi/v2/account'
    params = {
        'recvWindow': self._recv_window,
        'timestamp': binance_req_timestamp(),
    }
    update = self._query(method, path, params)
    return update

  def query_trade_list(
      self,
      symbol,
      start_time=None,
      end_time=None,
      from_id=None,
      limit=None,
  ):
    method = 'GET'
    path = '/fapi/v1/userTrades'

    if limit is not None:
      assert 0 < limit <= 1000, limit

    params = {
        'symbol': symbol,
        'startTime': start_time,
        'endTime': end_time,
        'fromId': from_id,
        'limit': limit,
        'recvWindow': self._recv_window,
        'timestamp': binance_req_timestamp(),
    }
    update = self._query(method, path, params)
    return update

  def query_position_risk(self):
    method = 'GET'
    path = '/fapi/v1/positionRisk'
    params = {
        'recvWindow': self._recv_window,
        'timestamp': binance_req_timestamp(),
    }
    update = self._query(method, path, params)
    return update

  def start_data_stream(self):
    method = 'POST'
    path = '/fapi/v1/listenKey'
    update = self._query(method, path)
    return update

  def keep_alive_data_stream(self, listen_key):
    method = 'PUT'
    path = '/fapi/v1/listenKey'
    params = {'listenKey': listen_key}
    update = self._query(method, path, params)
    return update

  def close_data_stream(self, listen_key):
    method = 'DELETE'
    path = '/fapi/v1/listenKey'
    params = {'listenKey': listen_key}
    update = self._query(method, path, params)
    return update
